<?php
// FileFind - ftp/shares indexing and search php engine,
// http://project.ffind.ru
//
// Module: crawler.php
// Description: Crawler
// Copyright (c) 2003-2008 Rage Steel, www.gt2.ru
// ----------------------------------------------
// $Id$

// *** Все обновлению индекса и пинга
set_time_limit(60 * 60 * 12); // 12 часов

chdir('../www');
require_once('./lib/lib.main.php');
$tpl['no_page_template'] = true;

require_once('./lib/class.sql.php');
require_once('./lib/class.host.php');

require_once('./lib/lib.listing.php');
require_once('./lib/lib.comparer.php');
require_once('./lib/lib.info.php');
require_once('./lib/class.dir.php');
require_once('./lib/class.ftp.php');

require_once('../crawl/class.host.crawl.php');
require_once('../crawl/lib.listing.stream.php');
//require_once('../crawl/lib.profile.php');

// ***************************************************************************

// Владельцы файлов/каталогов для шар
$owner_id_by_name = array();

// Найти или создать owner_id для owner в массиве
// Используется в CHostShareDir::read_dir()
function process_owners(&$files) {
  global $sqlconn, $owner_id_by_name;

  $find_owners = array();

  reset($files);
  while(list($file_name, $file) = each($files)) {
    $owner = strtoupper($file['owner']);

    if (isset($owner_id_by_name[$owner])) {
      // Такой владелец уже есть в нашем кэширующем массиве
      $files[$file_name]['owner_id'] = $owner_id_by_name[$owner];
    } else {
      // Добавляем в массив для поисков
      $find_owners[$owner] = '"' . $sqlconn->escape($owner) . '"';
    }
  }

  if (count($find_owners) > 0) {
    // *** По идее, тут надо блокировать таблицу
    $sq = new CSqlQ('tbl_owners');
    $sq->add_fields(array('owner_id', 'owner_name'));
    // Надеюсь, что мы не превысим значение max_allowed_packet, при построении строчки IN
    $sq->where = 'UPPER(`owner_name`) IN (' . implode(', ', $find_owners) . ')';
    $sq->exec_select();

    if ($sq->num_rows() > 0) {
      while(list($owner_id, $owner_name) = $sq->fetch_row()) {
        $owner_name = strtoupper($owner_name);
        $owner_id_by_name[$owner_name] = $owner_id;
        unset($find_owners[$owner_name]);
      }
    }

    if (count($find_owners) > 0) {
      // Если ещё что-то осталось, то значит это нечто нужно добавить в таблицу
      $sql = 'INSERT INTO `' . $sq->table . '` (`owner_name`) ' . 
        'VALUES(' . implode('), (', $find_owners) . ')';
      $sq->exec_sql($sql);

      $owner_id = $sq->last_insert_id();
      reset($find_owners);
      while(list($owner_name) = each($find_owners)) {
        $owner_id_by_name[$owner_name] = $owner_id;
        $owner_id++;
      }
    }
    // *** А где-то тут - разблокировать
  } else {
    return;
  }

  // Если мы тут, то значит надо повторно пройти по массиву файлов/каталогов и проставить недостающие owner_id
  reset($files);
  while(list($file_name, $file) = each($files)) {
    if (isset($file['owner_id'])) {
      continue;
    }

    $owner = strtoupper($file['owner']);
    if (isset($owner_id_by_name[$owner])) {
      // Такой владелец уже есть в нашем кэширующем массиве
      $files[$file_name]['owner_id'] = $owner_id_by_name[$owner];
    }
  }
} // process_owners

// ***************************************************************************

function print_arr(&$arr) {
  reset ($arr);
  while (list(, $line) = each($arr)) {
    echo convert_cyr_string($line, 'w', 'd'), "\n";
  }
} // print_arr

// ***************************************************************************

// Добавление узлов из коммандной строки
function walk_add($host_name) {
  global $opt;

  echo "Host '$host_name' ";

  $host = new CHost();

  if (RS_is_ip($host_name) != -1) {
    $host_name = gethostbyaddr($host_name);
  }

  if ($host->load($host_name, true)) {
    echo "is already exists as '$host_name', skipped.\n";
    return;
  }

  $conn_error = $host->check($host_name, $opt['host_type']);
  $create_host = false;

  if ($conn_error == 0) {
    $create_host = true;
    $log_action = 1;
    echo "check ok";
  } elseif ($opt['force']) {
    $create_host = true;
    $log_action = 2;
    echo "error $conn_error, but forced to be added";
  } else {
    echo "error $conn_error";
  }

  if ($create_host) {
    $host->create($host_name, $opt['host_type'], $host->host_ip);
    $host->log_action($log_action);
    echo ", added.\n";
  } else {
    echo ", skipped.\n";
  }
} // walk_add

// ***************************************************************************

function walk_kill($host_name) {
  global $opt;
  $host = new CHost();
  // TODO Удалять также картинки со статусом

  if (!$host->load($host_name, $opt['host_ip_search'])) {
    echo " $host_name does not exists, skipping\n";
  } else {
    echo " $host_name KILLING\n";
    echo "  Deleting files...\n";
    $host->kill_files();
    echo "  Deleting directories...\n";
    $host->kill_dirs();
    echo "  Deleting crawl log...\n";
    $host->kill_conn();
    echo "  Deleteing host...\n";
    $host->kill_host();
    echo "  Done\n";
  }
} // walk_kill

// ***************************************************************************

  // Проверка на существование всех lock_pid'ов
  function check_pid_exists() {

    // Получим массив pid => host_id
    $sq = new CSqlQ('tbl_hosts');
    // TODO А тут можно добавить условие для провеки процессов идущих хотя бы более часа
    // чтобы каждый раз не дёргать список задач

    // Из-за проблем с полученикм списка задач добавили условие при котором сброс происходит только через сутки
    $sq->where = '`lock_pid` IS NOT NULL AND `lock_datetime` + INTERVAL 1 DAY < NOW()';
    $sq->add_fields(array('host_id', 'lock_pid', 'lock_datetime'));
    $sq->exec_select();

    if ($sq->num_rows() == 0) {
      return 0;
    }

    $pids = array();
    while(list($host_id, $lock_pid, $lock_datetime) = $sq->fetch_row()) {
      $pids[$lock_pid] = array('host_id' => $host_id, 'lock_datetime' => $lock_datetime);
    }

    // Удалим из массива запущенные процессы
    exec('tasklist /FO LIST', $tasklist, $rc);

    reset($tasklist);
    while(list(, $line) = each($tasklist)) {
      // TODO Конечно, тут хорошо бы проверять не только на PID, но и сравнивать имя процесса с именем текущего процесса
      if (preg_match('/^PID\:\s+(\d+)$/', $line, $m)) {
        $task_pid = (int)$m[1];
        if (isset($pids[$task_pid])) {
          unset($pids[$task_pid]);
        }
      }
    }

    // Сбросим блокировку в оставшихся узлах
    reset($pids);
    while(list($pid, $data) = each($pids)) {
      $sq = new CSqlQ('tbl_hosts');
      $sq->add_value('host_id', $data['host_id']);
      $sq->add_value('lock_pid', $pid);
      $sq->add_value('lock_datetime', $data['lock_datetime']);
      $sq->fill_where(true);
      $sq->add_value('lock_pid', null);
      $sq->add_value('lock_datetime', null);
      $sq->add_calc_field('host_status', '`host_status` &~ (1 | 2 | 4 | 8)');

      $sq->exec_update();
    }
  } // check_pid_exists

  // Получить количество узлов с заданным $host_status
  function get_host_status_count($host_status) {
    $sq = new CSqlQ('tbl_hosts');
    $host_status = (int)$host_status;
    $sq->add_calc_field('cnt', 'COUNT(`host_id`)');
    $sq->where = "`host_status` & $host_status = $host_status";
    $sq->exec_select();
    if ($sq->num_rows() == 1) {
      list($cnt) = $sq->fetch_row();
      return $cnt;
    } else {
      return -1;
    }
  } // get_host_status_count
  
  // Установка бита need_crawl (2) всем нуждающимся узлам
  function set_need_crawl() {
    global $cfg;

    $sq = new CSqlQ('tbl_hosts');
    $sq->add_calc_field('host_status', '`host_status` | 2');
    $sq->where = '((`crawl_last` IS NULL) OR (NOW() > `crawl_last` + INTERVAL ' . $cfg['host_index_interval'] . ' SECOND))';
    // Условие на неблокированный узел отсутствует по той причине, что я не увидел в этом необходимости
    //  ' AND (`lock_pid` IS NULL)';
    $sq->exec_update();
  } // set_need_crawl

  // Получение следующего узла для опроса или индексации
  function get_next_host($is_ping) {
    global $cfg;

    $sq = new CSqlQ('tbl_hosts');
    $sq->add_field('host_id');

    if ($is_ping) {
      $w_crawl = '(`host_status` & 2 = 0)';
    } else {
      $w_crawl = '(`host_status` & 2 = 2)';
    }
    
    if (isset($host_group_id)) {
   	  // Обходим только узлы, которые входят в текущую группу
      $w_crawl .= 'AND (`group_id` = ' . $host_group_id . ')';
    }
    
    $sq->where = '((`conn_last` IS NULL) OR (NOW() > `conn_last` + INTERVAL ' . $cfg['host_ping_interval'] . ' SECOND)) AND ' . 
      "(`host_opt` & 1 = 0) AND (`lock_pid` IS NULL) AND $w_crawl";

    $sq->order_by = $is_ping ? '`conn_last` ASC' : '`crawl_last` ASC';
    $sq->limit = 1;
    $sq->exec_select();
    if ($sq->num_rows() == 0) {
      return -1;
    } else {
      list($host_id) = $sq->fetch_row();
      return $host_id;
    }
  } // get_next_host

// Вторая версия функции, построенная в соответствии с новыми требованиями
function do_ping_crawl2($hosts = null) {
  global $cfg, $sync_stat;

  check_pid_exists();
  set_need_crawl();
  $is_ping = true;

  if (is_array($hosts)) {
    $is_ping = false;
    reset($hosts);
  }

  while(true) {
    if (is_array($hosts)) {
      // Список узлов для индексации был задан из коммандной строки
      $host = new CHostCrawlStream(); // CHostCrawlStream
      list(, $host_name) = each($hosts);
      if (!$host_name) {
        break;
      }
      if (!$host->load($host_name)) {
        die("Error loading host with name=$host_name\n");
      }
    } else {
      // Получим $host_id для операции
      if ($is_ping) {
        $host_id = get_next_host(true);
        if (-1 == $host_id) {
          $is_ping = false;
          continue;
        }
      } else {
        $host_id = get_next_host(false);
        if (-1 == $host_id) {
          break;
        }
      }

      $host = new CHostCrawlStream($host_id);
      if ('' == $host->host_name) {
        die("Error loading host with host_id=$host_id\n");
      }
    }

    // Попробуем его заблокировать
    $set_status = $is_ping ? 1 : 4;
    if (!$host->lock($set_status)) {
      continue;
    }

    echo 'Host: ' . $host->host_name . ', ';
    if ($is_ping) {
      echo 'action: ping';
      $ping = $host->connect(0);
      echo ", result: $ping\n";

    } else {

      echo 'action: crawl';
      $ping = $host->get_listing($cfg['host_index_interval'] / 2);
      if (-1 == $ping) {
        $ping_r = 'listing ' . RS_fmt_datetime(strtotime($host->conn_datetime));
        $ping = 0;
      } else {
        $ping_r = $ping;
      } 
      echo ", result: ${ping_r}\n";

      if (0 == $ping) {
        $host->change_status('& ~4');
        
        if (get_host_status_count(8) >= $cfg['max_sync_count']) {
          echo '  Waiting for parallel sync process';

          while (get_host_status_count(8) >= $cfg['max_sync_count']) {
            sleep($cfg['waiting_sleep']);
            echo '.';
          }
          echo "\n";
        }

        echo '  Sync ';
        $host->change_status('| 8');

        if ($host->sync()) {
          $dur = RS_seconds01($sync_stat['dur']);
          $lines = $host->rawlist_count;
//dump_memory_usage();
          echo 
            "\n  Done in $dur seconds, processed $lines line(s)\n",
            '  Sync dirs: ',  $sync_stat['sync_count'],
            '; dirs+: ', $sync_stat['dirs_added'],
            '; dirs-: ', $sync_stat['dirs_removed'],
            '; files+: ', $sync_stat['files_added'],
            '; files-: ', $sync_stat['files_removed'],
            '; files*: ', $sync_stat['files_changed'],
            "\n",
            '   Errors: master load ', $sync_stat['err_master_load'],
            ', read ', $sync_stat['err_master_read'],
            '; slave load ', $sync_stat['err_slave_load'],
            ', read ', $sync_stat['err_slave_read'],
            "\n";
        } else {
          echo "  Error sync\n";
        }
      }
    }

    $reset_status = $is_ping ? 1 : (2 | 4 | 8);
    $host->unlock($reset_status);
  }
} // do_ping_crawl2

// ***************************************************************************

function do_scan_listing() {
	global $host_group_id;
	
	$dir = '../listing';
	$dh = opendir($dir);
	if (false === $dh) {
		return false;
	}
	  
	while($file = readdir($dh)) {
		$listing = explode('-', $file, 3);
		if (3 != count($listing)) {
			continue;
		}
		list($group_name, $host_type, $host_name) = $listing;
		
		
	}
	closedir($dh);
} // do_scan_listing

// ***************************************************************************

// use NET VIEW and check found computers for disk shares
function do_share_discover() {
  exec('net view', $result, $return);
  $hosts = array();

  reset($result);
  while(list(, $line) = each($result)) {
    if (preg_match('/^\\\\\\\\(\S+)\s*(\S+\s*)?$/', $line, $m)) {
      $hosts[] = $m[1];
      $owner = isset($m[2]) ? trim($m[2]) : '(unknown)';
    }
  }

  if (($cnt = count($hosts)) > 0) {
    echo "Found $cnt host(s)\n";
    array_walk($hosts, 'walk_try_share');
  } else {
    echo "No hosts found\n";
  }
} // do_share_discover

// ***************************************************************************

function rs_is_upper(&$str) {
  return $str == strtoupper($str);
} // rs_is_upper

function rs_is_lower(&$str) {
  return $str == strtolower($str);
} // rs_is_lower

// Проверка на смешенный регистр
function rs_is_mix(&$str) {
  return (!rs_is_upper($str)) && (!rs_is_lower($str));
} // rs_is_mix

function rs_is_ucfirst(&$str) {
  return $str == ucfirst($str);
} // rs_is_ucfirst

function rs_is_ucwords(&$str) {
  return $str == ucwords($str);
} // rs_is_ucwords

function do_fix_owners() {
  global $cfg;

  $sq = new CSqlQ('tbl_owners');
  $sq->add_fields(array('owner_id', 'owner_name'));
  $sq->order_by = 'UPPER(`owner_name`)';
  $sq->exec_select();
  if ($sq->num_rows() == 0) {
    return true;
  }

  $prev_owner_name = '';
  $owner_arr = array();
  while(list($owner_id, $owner_name) = $sq->fetch_row()) {
    if ($prev_owner_name != strtoupper($owner_name)) {
      if (count($owner_arr) > 1) {
        // *** Необхожимо оставить только одного владельца

        // Сначала нужно решить, какое имя оставить
        // Вводим особое понятие - качество имени
        $q_owner_dom = array();
        $q_owner_usr = array();
        reset($owner_arr);
        while(list($owner_id, $owner_name) = each($owner_arr)) {
          $owner = explode("\\", $owner_name, 2);

          $q_owner_dom[$owner_id] = 
            1 * rs_is_upper($owner[0]) +
            2 * rs_is_mix($owner[0]) +
            4 * rs_is_ucfirst($owner[0]) +
            8 * rs_is_ucwords($owner[0]);

          $q_owner_usr[$owner_id] =
            1 * rs_is_upper($owner[1]) +
            2 * rs_is_mix($owner[1]) +
            4 * rs_is_ucfirst($owner[1]) +
            8 * rs_is_ucwords($owner[1]);
        }

        $max_dom = max($q_owner_dom);
        $max_usr = max($q_owner_usr);
        $dom_id = array_search($max_dom, $q_owner_dom);
        $usr_id = array_search($max_usr, $q_owner_usr);


        if ($dom_id && $usr_id) {
          $dom_arr = explode("\\", $owner_arr[$dom_id], 2);
          $dom = $dom_arr[0];
          $usr_arr = explode("\\", $owner_arr[$usr_id], 2);
          $usr = $usr_arr[1];
          $new_owner_name = $dom . "\\" . $usr;
        } else {
          die("Error constructing new owner name!\n");
        }

        // Быстрая проверочка получившегося имени:
        $qc = true;
        reset($owner_arr);
        while(list($owner_id, $owner_name) = each($owner_arr)) {
          if (strtoupper($owner_name) != strtoupper($new_owner_name)) {
            $qc = false;
            break;
          }
        }

        if (!$qc) {
          die("New owner name check failed: $new_owner_name\n");
        }

        // Имя получили - теперь поместим его в запись на наименьшим $owner_id
        $new_owner_id = min(array_keys($owner_arr));

        $owner_list = implode(', ', $owner_arr);
        echo "From names $owner_list built name $new_owner_name\n";

        $squ = new CSqlQ('tbl_owners');
        $squ->add_value('owner_id', $new_owner_id);
        $squ->fill_where(true);
        $squ->add_value('owner_name', $new_owner_name);
        $squ->exec_update();
        unset($squ);

        // Исключим из массива owner_id, к которому мы будем всё приводить
        unset($owner_arr[$new_owner_id]);
        $where = '`owner_id` IN (' . implode(', ', array_keys($owner_arr)) . ')';

        // Поменяем owner_id в файлах и каталогах
        $change_tbl = array('tbl_files', 'tbl_dirs');

        reset($change_tbl);
        while(list(, $tbl) = each($change_tbl)) {
          $squ = new CSqlQ($tbl);
          $squ->add_value('owner_id', $new_owner_id);
          $squ->where = $where;
          $squ->exec_update();
          unset($squ);
        }

        $sqd = new CSqlQ('tbl_owners');
        $sqd->where = $where;
        $sqd->exec_delete();
        unset($sqd);

      } // count($owner_arr) > 1
      $owner_arr = array($owner_id => $owner_name);
    } else {
      $owner_arr[$owner_id] = $owner_name;
    }
    $prev_owner_name = strtoupper($owner_name);
  }

  // Также надо будет найти "безродных" и "лишних" владельцев в таблице владельцев
  $sqw = new CSqlQ('tbl_owners');
  $sqw->add_field('owner_name');


  $check_tbl = array('tbl_files' => 'file_id', 'tbl_dirs' => 'dir_id');
  // Количество использования имён владельцев
  $used_counts = array();
  reset($check_tbl);
  while(list($tbl, $pk) = each($check_tbl)) {
    $sqf = new CSqlQ($tbl);
    $sqf->add_field($pk);
    
    $sqf->left_join_using($sqw, '`owner_id`');
    $sqf->where = '`owner_name` IS NULL AND `' . $cfg[$tbl] .'`.`owner_id` IS NOT NULL';

    $sqf->exec_select();

    $cnt = $sqf->num_rows();
    echo "Orhaned owners in $tbl: $cnt\n";

    $squ = new CSqlQ($tbl);
    $squ->add_field('owner_id');
    $squ->add_calc_field('cnt', 'COUNT(`owner_id`)');
    $squ->group_by = '`owner_id`';
    $squ->exec_select();
    if ($squ->num_rows() > 0) {
      while(list($owner_id, $cnt) = $squ->fetch_row()) {
        if (is_null($owner_id)) {
          continue;
        }

        if (isset($used_counts[$owner_id])) {
          $used_counts[$owner_id] += $cnt;
        } else {
          $used_counts[$owner_id] = $cnt;
        }
      }
    }
  }

  if (count($used_counts) > 0) {
    $squ = new CSqlQ('tbl_owners');
    $squ->add_fields(array('owner_id', 'owner_name'));
    $squ->where = '`owner_id` NOT IN (' . implode(', ', array_keys($used_counts)) . ')';
    $squ->exec_select();

    if ($squ->num_rows() > 0) {
      while(list($owner_id, $owner_name) = $squ->fetch_row()) {
        echo "unused $owner_id $owner_name\n";
      }
    }
  }

} // do_fix_owners


// ***************************************************************************

// $activate - True - активировать, False - деактивировать
function do_activate_deactivate($host_list, $activate, $reason, $hact_type, $log_dt) {
  $hosts = array();
  $hosts_id = array();

  reset($host_list);
  while(list(, $host_id) = each($host_list)) {
    // Блокировка узлов
    $host = new CHostCrawl($host_id);
    if (!$host->lock()) {
      // Занятые - пропускаем
      echo "Host ", $host->host_name, " is locked, skipping...\n";
      continue;
    }

    $hosts[] = $host;
    $hosts_id[] = $host->host_id;
    log_host_action($host_id, $hact_type, $log_dt);
  }

  $hosts_where = '`host_id` IN (' . implode(', ', $hosts_id) . ')';
  $cnt = count($hosts);

  if ($activate) {
    echo "Hosts to activate: $cnt\n";
    reset($hosts);
    while(list(, $host) = each($hosts)) {
      $host->make_active();
    }
 
  } else {
    echo "Hosts to deactivate: $cnt\n";

    // Установить флаг неактивности
    $sq = new CSqlQ('tbl_hosts');
    $sq->add_calc_field('host_opt', '`host_opt` | (1 | ' . (int)$reason . ')');
    $sq->where = $hosts_where;
    $sq->exec_update();

    // Удалить все файлы
    echo "Killing files...\n";
    $sq = new CSqlQ('tbl_files');
    $sq->where = $hosts_where;
    $sq->exec_delete();

    // Удалить все каталоги, включая корневой
    echo "Killing directories...\n";
    $sq = new CSqlQ('tbl_dirs');
    $sq->where = $hosts_where;
    $sq->exec_delete();
  }

  // Разблокировать узлы
  reset($hosts);
  while(list(, $host) = each($hosts)) {
    // Прибить оставшиеся листинги
    
    // Не... пусть останется хотя-бы последний листинг
    // $host->kill_rawlist($host->listing_file);
    $host->kill_rawlist($host->listing_file_tmp);

    $host->unlock();
  }

} // do_activate_deactivate

// Деактивация узлов
// $host_list - array узлов, подлежащих деактивации
// $reason - причина деактивации, см. host.txt
// $hact_type - тип действия, для записи в журнал узла, см. host.txt
function do_deactivate($host_list, $reason, $hact_type, $log_dt) {
  do_activate_deactivate($host_list, false, $reason, $hact_type, $log_dt);
} // do_deactivate

// Ежедневная проверка узлов.
// Устаревшие узлы деактивировать.
// Провести оптимизацию таблиц.
function do_day_check() {
  global $cfg;

  // Проверка на старость
  $check_dt = get_sql_datetime();

  $sq = new CSqlQ('tbl_hosts');
  $sq->add_field('host_id');
  $sq->where = '(`host_opt` & 1 = 0) ' . 
    'AND (' . 
      '((`crawl_last` IS NOT NULL) AND (`crawl_last` + INTERVAL 30 DAY < NOW()))' .
      ' OR ' .
      '((`conn_ok` IS NOT NULL) AND (`conn_ok` + INTERVAL 30 DAY < NOW()))' . 
      ' OR ' .
      '(((`crawl_last` IS NULL) OR (`conn_ok` IS NULL)) AND (`date_add` + INTERVAL 30 DAY < NOW()))' .
    ')';
  $sq->exec_select();

  if ($sq->num_rows() > 0) {
    $hosts = array();

    while(list($host_id) = $sq->fetch_row()) {
      $hosts[] = $host_id;
    }
    do_deactivate($hosts, 2, 5, $check_dt);
  }

  // Удаление старых поисковых результатов
  // Сначала надо определить пределы удаляемого - снизу - минимум с query_status = 0, сверху - самый новый, но попадающий под действия интервала
  $sq = new CSqlQ('tbl_query');
  $sq->add_calc_field('qmax', 'MAX(`query_id`)');
  $sq->where = '`query_datetime` + INTERVAL 1 DAY < NOW() AND (`query_status` & 1) = 0';
 
  $sq->order_by = '`query_id` ASC';

  $sq->exec_select();
  list($qmax) = $sq->fetch_row();
  if (!is_null($qmax)) {
    echo "Processing results...\n";
    $where = "`query_id` <= $qmax";

    echo "Killing old results...\n";
    $sq = new CSqlQ('tbl_res');
    $sq->where = $where;
    $sq->exec_delete();
    $ri = $sq->affected_rows();

    $sq = new CSqlQ('tbl_wres');
    $sq->where = $where;
    $sq->exec_delete();
    $wi = $sq->affected_rows();

    echo "Updating query table...\n";
    $sq = new CSqlQ('tbl_query');
    $sq->where = $where;
    $sq->add_calc_field('query_status', '`query_status` | 1');
    $sq->exec_update();
    $qi = $sq->affected_rows();

    echo "Done, updated $qi queries, removed $ri result and $wi work result items\n";
  }

  // Оптимизация таблиц
  echo "Optimizing tables...\n";
  reset($cfg['all_tables']);
  while(list(, $tbl) = each($cfg['all_tables'])) {
    $sq = new CSqlQ($tbl);
    echo "  table $tbl:\n";
    $sq->exec_optimize();
    if ($sq->num_rows() > 0) {
      while(list($table, $op, $msg_type, $msg_text) = $sq->fetch_row()) {
        echo "    $msg_type: $msg_text\n";
      }
    }
  }
} // do_day_check

// Сформировать статистику за прошлый день и отправить её по электронной почте
function do_mail_stat() {
  global $cfg, $sqlconn;

  echo "Collection statistics...\n";
  // Сначала определим какое число сегодня и какое было вчера 
  $sq = new CSqlQ();
  $sq->add_calc_field('today', 'CURDATE()');
  $sq->add_calc_field('yesterday', 'CURDATE() - INTERVAL 1 DAY');
  $sq->exec_select();

  list($today, $yesterday) = $sq->fetch_row();
  $where = " BETWEEN '$yesterday' AND '$today'";

  // Подсчитаем количество запросов страниц
  // Количество ошибок SQL
  $sq = new CSqlQ('tbl_preq');
  $sq->add_calc_field('count', 'COUNT(*)');
  $sq->add_calc_field('page_time', 'SUM(`req_pgt`)');
  $sq->add_calc_field('sql_time', 'SUM(`req_sqt`)');
  $sq->add_calc_field('sql_err', 'SUM((`req_flags` & 64) <> 0)');

  $sq->where = "`req_dt` $where";
  $sq->exec_select();

  list($preq, $page_time, $sql_time, $sql_err) = $sq->fetch_row();
  $page_time_fmt = RS_seconds01($page_time);
  $sql_time_fmt = RS_seconds01($sql_time);

  if (($page_time + $sql_time) > 0) {
    $sql_prc = number_format(($sql_time / $page_time) * 100, 2);
  } else {
    $sql_prc = 0;
  }

  // Общее количество запросов
  $search_info = get_search_info($today);
  $total_q = $search_info['count'];
  if ($total_q > 0) {
    $start_date = RS_fmt_datetime(strtotime($search_info['start_date']));
  } else {                        
    $start_date = '(неприменимо)';
  }

  // Количество поисковых запросов
  // Общее время выполнения поисковых запросов (dur_* в таблице поисковых запросов)
  // Т.о. найдём среднюю проложительность запроса
  // Количество незавершённых запросов
  $sq = new CSqlQ('tbl_query');
  $sq->add_calc_field('count', 'COUNT(*)');
  $sq->add_calc_field('duration', 'SUM(IFNULL(`dur_dirs`, 0) + IFNULL(`dur_files`, 0) + IFNULL(`dur_sort`, 0))');
  $sq->where = "`query_datetime` $where";
  $sq->exec_select();

  list($q_count, $q_dur) = $sq->fetch_row();
  $yesterday_fmt = RS_fmt_date(strtotime($yesterday));
  $ver = FF_VERSION;
  $q_dur_fmt = RS_seconds01($q_dur);

  if ($q_dur > 0) {
    $q_speed = number_format($q_count / ($q_dur / 10), 3);
  } else {
    $q_speed = '(неприменимо)';
  }

  // Количество обращений к узлам
  $sq = new CSqlQ('tbl_conn');
  $sq->add_calc_field('count', 'COUNT(*)');
  $sq->add_calc_field('duration', 'SUM(`conn_dur`)');
  $sq->where = "`conn_datetime` $where";
  $sq->exec_select();
  list($c_count, $c_dur) = $sq->fetch_row();
  $c_dur_fmt = RS_seconds01($c_dur);

  // Количество индексаций
  $sq = new CSqlQ('tbl_crawl');
  $sq->add_calc_field('count', 'COUNT(*)');
  $sq->add_calc_field('duration', 'SUM(`crawl_dur`)');
  $sq->add_calc_field('lines', 'SUM(`crawl_count`)');
  $sq->where = "`crawl_datetime` $where";
  $sq->exec_select();
  list($i_count, $i_dur, $i_lines) = $sq->fetch_row();
  $i_dur_fmt = RS_seconds01($i_dur);
  if ($i_dur > 0) {
    $i_speed = number_format($i_lines / ($i_dur / 10), 1);
  } else {
    $i_speed = '(неприменимо)';
  }

  // Информация о системе
  $info = get_system_info();
  $os_info = &$info['os'];
  $php_info = &$info['php'];
  $php_exensions = implode(', ', $php_info['extensions']);
  $mysql_info = &$info['mysql'];

  $server = $cfg['server_name'];

  $dur_stat = number_format(RS_time_passed(), 2);

  $msg = <<< EOT
Здравствуйте, уважаемый господин Администратор!

Хочу сообщить вам, что творилось на сервере $server, вчера, т.е. $yesterday_fmt.

Количество запрошенных с сервера страниц: $preq;
Время на генерацию страниц: $page_time_fmt;
Время на запросы для генерации: $sql_time_fmt ($sql_prc %);
Количетсво ошибок в SQL-запросах: $sql_err;

Количество поисковых запросов: $q_count;
Продолжительность поисковых запросов: $q_dur_fmt;
Средний темп обработки, запросов/секунду: $q_speed;

Общее количество поисковых запросов: $total_q начиная с $start_date;

Количество подключений к ресурсам: $c_count;
Суммарное время на все подключения: $c_dur_fmt;

Количество индексаций: $i_count;
Суммарное время на все индексации: $i_dur_fmt;
Средний темп индексации, строк/секунду: $i_speed;

Операционная система:
  Узел:     $os_info[host];
  Название: $os_info[name];
  Версия:   $os_info[version];
  Сборка:   $os_info[release];
  Тип CPU:  $os_info[machine];

PHP: 
  Версия:     $php_info[version];
  Расширения: $php_exensions;

MySQL: 
  Клиент:   $mysql_info[client_info];
  Сервер:   $mysql_info[host_info];
  Протокол: $mysql_info[proto_info];
  Узел:     $mysql_info[server_info];

Время формирования статистики, сек.: $dur_stat;

С уважением,
  filefind/$ver
EOT;

  if (isset($cfg['pop_before_smtp'])) {
    $pop3 = $cfg['pop_before_smtp']['server'];
    $user = $cfg['pop_before_smtp']['user'];
    $pass = $cfg['pop_before_smtp']['pass'];

    echo "Doing pop before smtp... ";
    $imap = imap_open('{' . $pop3 . ':110/pop3}INBOX', $user, $pass);
    if (!$imap) {
      echo 'failed';
      die();
    } else {
      echo 'ok';
    }
    echo "\n";

    imap_close($imap);
  }

  echo 'Sending message... ';

  $headers = "From: filefind@$server";

  $addr = $cfg['admin_email'];
  if (is_array($addr)) {
    $msg_to = array_shift($addr);
    if (count($addr) > 0) {
      $headers .= "\r\nCc: " . implode(', ', $addr);
    }
  } else {
    $msg_to = $addr;
  };

  $subj = "$server $yesterday_fmt статистика filefind-$ver";

  if (mail($msg_to, 
    convert_cyr_string($subj, 'w', 'k'), 
    convert_cyr_string($msg, 'w', 'k'),
    $headers)) {
    echo 'ok';
  } else {
    echo 'failed';
  }

} // do_mail_stat

// ***************************************************************************

// Получить "местный" путь, для какого-либо файла
function get_local_path($dir_id, $file_name) {
  global $dir_by_id;

  $path = array();
  
  while(!is_null($dir_id)) {
    $path[] = $dir_by_id[$dir_id]['dir_name'];
    $dir_id = $dir_by_id[$dir_id]['par_dir_id'];
  }
  
  $str = $path[count($path) - 2];
  $str = substr($str, 0, 1) . ':\\';

  for ($i = count($path) - 3; $i >= 0; $i--) {
    $str .= $path[$i] . '\\';
  }


  return $str . $file_name;
} // get_local_path

// Запрос файлов для хеширования
// пока только >= 64M
function do_hash_query($host_name) {
  $exceptions = array(
    'PAGEFILE.SYS' => 0,
    'HIBERFIL.SYS' => 0,
    'DRIVER.CAB' => 0, 
    'WIN386.SWP' => 0
  );
  $min_file_size = 1048576; // 1 * 1024 * 1024 // 67108864; // 64 * 1024 * 1024

  // TODO вообще-то нужно "запирать" узел
  $host = new CHost();
  if (!$host->load($host_name)) {
    die('Unknown host name');
  };
  $host_id = $host->host_id;

  echo "host_id: $host_id\n";

  $calc_done = array();

  $hash_file = "../filehash/$host_name.hsh";
  if (file_exists($hash_file)) {
    $f = fopen($hash_file, 'r');

    
    while(!feof($f)) {
      $line = rtrim(fgets($f));

//  1  2    3  4  5      6                            7
// 14.10.2004 12:19 739532 C:\App\openbiz2004-10-14.sql
      $lt1 = preg_match('/^(\d{2})\.(\d{2})\.(\d{2,4}) (\d{2})\:(\d{2}) {1,2}(\d+) (.+)$/', $line, $m1);
// 1                                         2
// 60b42e53c54fdc410b9f1d21b4ce170c ?EDONKEY*openbiz2004-10-14.sql
      $lt2 = preg_match('/^([0-9a-f]{32}) \?EDONKEY\*(.+)$/', $line, $m2);

      if (!$lt1 & !$lt2) {
        continue;
      }

      if ($lt1) {
        $my_dt = "$m1[3]-$m1[2]-$m1[1] $m1[4]:$m1[5]:00";
        $size = $m1[6];
        $path = $m1[7];
      }

      if ($lt2) {
        $file_name = convert_cyr_string($m2[2], 'w', 'd');
        if ($file_name != substr($path, -strlen($file_name))) {
          die ('hash_file error!');
        }
        $calc_done[$path] = array('file_name' => $file_name, 'file_size' => $size, 
          'file_datetime' => $my_dt, 'file_hash' => $m2[1]);
      }

    }
    fclose($f);
  }

  $hash_records = count($calc_done);
  echo "There was $hash_records usable hash(es) in hash file\n";

  $hq = new CSqlQ('tbl_fhash');
  $hq->add_fields(array('file_size', 'file_datetime', 'file_hash'));

  $sq = new CSqlQ('tbl_files');
  $tbl = $sq->table;
  $sq->where = "`host_id` = $host_id AND `$tbl`.`file_size` > $min_file_size";
  $sq->add_fields(array('file_id', 'dir_id', 'file_name', 'file_size', 'file_datetime'));

  $sq->left_join_using($hq, 'file_id');
  $sq->exec_select();

  $dir_id_arr = array();
  $files = array();
  while(list($file_id, $dir_id, $file_name, $size1, $dt1, $size2, $dt2, $hash) = $sq->fetch_row()) {
    if (isset($exceptions[strtoupper($file_name)])) {
      continue;
    }
    if (($size1 == $size2) && ($dt1 == $dt2)) {
      // Размер и/или дата/время поменялись, нужно пересчитать хэш
      continue;
    }
    $dir_id_arr[] = $dir_id;
    $files[$file_id] = array('dir_id' => $dir_id, 'file_name' => $file_name, 
      'file_size' => $size1, 'file_datetime' => $dt1, 'hash_exists' => !is_null($hash));
  }

  fill_dir_id($dir_id_arr);

  $hash_it = array();
  reset($files);
  while(list($file_id, $file_data) = each($files)) {
    $file = get_local_path($file_data['dir_id'], $file_data['file_name']);
    $file = convert_cyr_string($file, 'w', 'd');

    if (isset($calc_done[$file])) {
      $done = $calc_done[$file];
      if (($file_data['file_size'] != $done['file_size']) || ($file_data['file_datetime'] != $done['file_datetime'])) {
        // Файл поменялся, обновим информацию о нём
        $fu = new CSqlQ('tbl_files');
        $fu->add_value('file_id', $file_id);
        $fu->fill_where(true);
        $fu->add_value('file_size', $done['file_size']);
        $fu->add_value('file_datetime', $done['file_datetime']);
        $fu->exec_update();
      }
      $hu = new CSqlQ('tbl_fhash');

      $hu->add_value('file_id', $file_id);
      if ($file_data['hash_exists']) {
        $hu->fill_where(true);
      }
      $hu->add_value('file_size', $done['file_size']);
      $hu->add_value('file_datetime', $done['file_datetime']);
      $hu->add_value('file_hash', $done['file_hash']);

      if ($file_data['hash_exists']) {
        $hu->exec_update();
      } else {
        $hu->exec_insert();
      }

    } else {
      $hash_it[] = $file;
    }
  }

  if ($f = fopen("../filehash/$host_name.lst", 'w')) {
    reset($hash_it);
    while(list(, $line) = each($hash_it)) {
      fwrite($f, "$line\n");
    }
    fclose($f);
  }
} // do_hash_query

// ***************************************************************************

function do_show_hash_dupes() {
  global $host_by_id;

  $fh = new CSqlQ('tbl_fhash');
  $fh->add_fields(array('file_id', 'file_hash'));
  $fh->order_by = '`file_hash`, `host_id`';

  $fq = new CSqlQ('tbl_files');
  $fq->add_fields(array('file_name', 'dir_id', 'host_id'));

  $fh->left_join_using($fq, '`file_id`');

  $fh->exec_select();
  $dupes = array();
  $dir_id_arr = array();
  $orhaned = 0;
  while(list($file_id, $file_hash, $file_name, $dir_id, $host_id) = $fh->fetch_row()) {
    if (is_null($file_name)) {
      $orhaned++;
      continue;
    }
    $cur_file = array('file_id' => $file_id, 'file_name' => $file_name, 
       'dir_id' => $dir_id, 'host_id' => $host_id);
    if (isset($prev_hash)) {
      if ($prev_hash == $file_hash) {
        $dupes[] = $cur_file;
        $dupes[] = $prev_file;
        $dir_id_arr[] = $dir_id;
        $dir_id_arr[] = $prev_file['dir_id'];
      }
    }
    $prev_hash = $file_hash;
    $prev_file = $cur_file;
  }
  fill_dir_id($dir_id_arr);
  fill_host_id();

  reset($dupes);
  while(list(, $data) = each($dupes)) {
    $file = get_local_path($data['dir_id'], $data['file_name']);
    $file = convert_cyr_string($file, 'w', 'd');
    $host = $host_by_id[$data['host_id']]['host_name'];
    echo "dupe found: $host $file\n";
  }
  echo "Where was $orhaned orhaned file hash(es) found.\n";
}

// ***************************************************************************

// names ->
//// Check integerity of names table
//function do_check_names() {
//  // Проверка на соответствие name_cnt в таблице имён
//  // неизвестное имя - ставить какой-нибуь другой, определённые name_id
//  //  (можно было-бы и удалить, но если это каталог, то может много другого
//  //   содержать, поэтому лучше завести какой-нибудь 1, который будем считать
//  //   идентификатором файлов/каталогов потерявших лицо)
//  // Неправильное количество - исправлять
//  // name_cnt=0 - удалить
//
//  // Запускать сразу три запроса, сами имена и name_id в файлах и каталогах
//  //  GROUP BY `name_id` и ORDER BY `name_id` ASC
//
//  // И параллельно идём по ним.
//  // НЕТ! Один запрос на всё про всё, если получится.
//  // Не получится, так мы не найдём потерявших имя.
//
//
//} // do_check_names
//
//// ***************************************************************************
// <- names

// Возрвращает список идентификаторов узлов из простого списка узлов
function get_hosts_id($hosts) {
  global $sqlconn;

  $hosts_q = array();
  reset($hosts);
  while(list(, $host_name) = each($hosts)) {
    $hosts_q[] = '"' . $sqlconn->escape($host_name) . '"';
  }

  $sq = new CSqlQ('tbl_hosts');
  $sq->add_field('host_id');
  $sq->where = '`host_name` IN (' . implode(', ', $hosts_q) . ')';

  $sq->exec_select();

  $result = array();
  while(list($id) = $sq->fetch_row()) {
    $result[] = $id;
  }

  return $result;
} // get_hosts_id

$opt = array(
  'from_listing' => false,
  'host_ip_search' => true,
  'host_type' => HOST_TYPE_FTP,
  'force' => false
);

$commands = array(
  'add' => array('host-must-not-exist' => true),
  'activate' => array(),
  'deactivate' => array(),
  'kill' => array(),
  'fix-owners' => array('no-host-list' => true),
  'day-check' => array('no-host-list' => true),
  'hash-query' => array(),
  'show_hash_dupes' => array('no-host-list' => true),
  'crawl' => array(),
  'mail-stat' => array('no-host-list' => true),
  'help' => array('no-host-list' => true),
  'scan-listing' => array('no-host-list' => true)
);

// REFACTORING Это можно оформить в виде отдельной функции / или библиотеки

// Следующие два фрагмента кода - это борьба с отсутствием register_argc_argv
if (!isset($argc)) {
  if (isset($_SERVER['argc'])) {
    $argc = $_SERVER['argc'];
  } else {
    $argc = count($_GET) + 1;
  }
}

if (!isset($argv)) {
  if (isset($_SERVER['argv'])) {
    $argv = $_SERVER['argv'];
  } else {
    $argv = array();
    if (isset($_SERVER['SCRIPT_NAME'])) {
      $argv[0] = $_SERVER['SCRIPT_NAME'];
    } else {
      $argv[0] = '';
    }

    reset($_GET);
    while(list($n, $v) = each($_GET)) {
      if ($v != '') {
        $argv[] = $n . '=' . $v;
      } else {
        $argv[] = $n;
      }
    }
  }
}

if ($argc > 1) {
  $command = $argv[1];

  if (!isset($commands[$command])) {
    die("Unknown command: $command\n");
  }
  $cmd_opt = $commands[$command];

  $hosts = array();

  for ($i = 2; $i < $argc; $i++) {
    $str = $argv[$i];
    if ($str == '--from-listing') {
      $opt['from_listing'] = true;
    } elseif ($str == '--no-ip-search') {
      $opt['host_ip_search'] = false;
    } elseif ($str == '--ftp') {
      $opt['host_type'] = HOST_TYPE_FTP;
    } elseif ($str == '--share') {
      $opt['host_type'] = HOST_TYPE_SHARE;
    } elseif ($str == '--force') {
      $opt['force'] = true;
    } else {
      $hosts[] = $str;
    }
  }

  // Прибивать дубликаты
  $hosts = array_unique($hosts);

  $need_args = !isset($cmd_opt['no-host-list']);
  if ((count($hosts) == 0) && $need_args) {
    die("Error: command $command require arguments --- at least one hostname\n");
  } else {
    echo "Command: $command";
    if (count($hosts) > 0) {
      echo ', hosts ' . implode(', ', $hosts);
    }
    echo "\n";

//    if (!isset($cmd_opt['host-must-not-exist'])) {
//      if (count($hosts_id) == 0) {
//        die("No hosts not found!\n");
//      }
//    }

    if ($command == 'add') {
      array_walk($hosts, 'walk_add');

    } elseif ($command == 'activate') {
      $log_dt = get_sql_datetime();
      do_activate_deactivate(get_hosts_id($hosts), true, 0, 8, $log_dt);

    } elseif ($command == 'deactivate') {
      $log_dt = get_sql_datetime();
      do_deactivate(get_hosts_id($hosts), 0, 4, $log_dt);

    } elseif ($command == 'kill') {
      array_walk($hosts, 'walk_kill');

    } elseif ($command == 'share-discover') {
      do_share_discover();
// names ->
//    } elseif ($command == 'check_names') {
//      do_check_names();
// <- names
    } elseif ($command == 'fix-owners') {
      do_fix_owners();

    } elseif ($command == 'day-check') {
      do_day_check();   
    } elseif ($command == 'mail-stat') {
      do_mail_stat();
    } elseif ($command == 'hash-query') {
      require_once('./lib/lib.tree.php');
      do_hash_query($hosts[0]);
    } elseif ($command == 'show_hash_dupes') {
      require_once('./lib/lib.tree.php');
      do_show_hash_dupes(); 
    } elseif ($command == 'crawl') {
      do_ping_crawl2($hosts);
    } elseif ('help' == $command) {
      do_help();
    } elseif ('scan-listing' == $command) {
      do_scan_listing();
    } else {
      die("Unhandled command: $command\n");
    }
  }
} else {
  do_ping_crawl2();
}
// Список команд
function do_help() {
  $help = <<< EOH
add [--ftp|--share] [--force] HOSTNAME1 HOSTNAME2 ...
  Добавление узлов в список

share-discover
  Поиск и добавление новых узлов из сетевого окружения

kill [--no-ip-search] HOSTNAME1 HOSTNAME2 ...
  Удаление узлов

fix-owners
  Починка владельцев файлов

mail-stat
  Отправка ежедневной статистики по электронной почте

day-check
  Проверка не нужно-ли что-нибудь деактивировать по прошествию срока...

hash-query
  Работам с хэшами больших файлов
  
scan-listing
  Добавление узлов, существующих в каталоге listing и индексация их
EOH;
echo convert_cyr_string($help, 'w', 'd');
// Исключенные команды
// listing  HOSTNAME1 HOSTNAME2 ...
// ping HOSTNAME1 HOSTNAME2 ...
// crawl [--from-listing] HOSTNAME1 HOSTNAME2 ...


// Планы:
// Тип узла по умолчанию - в конфиг
// activate HOSTNAME1 HOSTNAME2 ...
// deactivate HOSTNAME1 HOSTNAME2 ...
}

?>